﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;

namespace Wheels
{
    public static class EmitUtility
    {
#pragma warning disable 1591

        #region


        private static readonly MethodInfo smCombineDelegateMethod;
        private static readonly MethodInfo smRemoveDelegateMethod;
        private const string addPrefix = "add_";
        private const string removePrefix = "remove_";
        private const MethodAttributes EventAccessorBindingFlags = MethodAttributes.SpecialName | MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final;

        private const MethodAttributes InterfaceMethodAttributes = MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Virtual | MethodAttributes.Final;

        #endregion

        static EmitUtility()
        {
            smCombineDelegateMethod = Types.Delegate.GetMethod("Combine", new[] { Types.Delegate, Types.Delegate });
            smRemoveDelegateMethod = Types.Delegate.GetMethod("Remove", new[] { Types.Delegate, Types.Delegate });
        }

        public static CustomAttributeBuilder ToBuilder(this CustomAttributeData target)
        {
            var namedFieldArguments = target.NamedArguments.Where(a => a.MemberInfo is FieldInfo).ToArray();
            var namedPropertieArguments = target.NamedArguments.Where(a => a.MemberInfo is PropertyInfo).ToArray();

            return new CustomAttributeBuilder(
                target.Constructor,
                target.ConstructorArguments.Select(a => a.Value).ToArray(),
                namedPropertieArguments.Select(a => a.MemberInfo).Cast<PropertyInfo>().ToArray(),
                namedPropertieArguments.Select(a => a.TypedValue.Value).ToArray(),
                namedFieldArguments.Select(a => a.MemberInfo).Cast<FieldInfo>().ToArray(),
                namedFieldArguments.Select(a => a.TypedValue.Value).ToArray());
        }

        public static CustomAttributeBuilder[] GetCustomAttributeBuilders(this Assembly target)
        {
            return CustomAttributeData.GetCustomAttributes(target).Select(a => a.ToBuilder()).ToArray();
        }
        public static CustomAttributeBuilder[] GetCustomAttributeBuilders(this Module target)
        {
            return CustomAttributeData.GetCustomAttributes(target).Select(a => a.ToBuilder()).ToArray();
        }
        public static CustomAttributeBuilder[] GetCustomAttributeBuilders(this MemberInfo target)
        {
            return CustomAttributeData.GetCustomAttributes(target).Select(a => a.ToBuilder()).ToArray();
        }
        public static CustomAttributeBuilder[] GetCustomAttributeBuilders(this ParameterInfo target)
        {
            return CustomAttributeData.GetCustomAttributes(target).Select(a => a.ToBuilder()).ToArray();
        }

        public static AssemblyBuilder MakeDynamicAssemblyStub(this AppDomain appDomain, Assembly reference, string assemblyName)
        {
            return appDomain.MakeDynamicAssemblyStub(reference, new AssemblyName(assemblyName));
        }
        public static AssemblyBuilder MakeDynamicAssemblyStub(this AppDomain appDomain, Assembly reference, AssemblyName assemblyName)
        {
            return appDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run, reference.GetCustomAttributeBuilders());
        }

        public static MethodBuilder CreateEventAccessor(this TypeBuilder typeBuilder, FieldInfo eventField, EventAccessorType accessorType)
        {
            string accessorPrefix;
            MethodInfo delegateAction;

            switch (accessorType)
            {
                case EventAccessorType.Add:
                    accessorPrefix = addPrefix;
                    delegateAction = smCombineDelegateMethod;
                    break;
                case EventAccessorType.Remove:
                    accessorPrefix = removePrefix;
                    delegateAction = smRemoveDelegateMethod;
                    break;
                default:
                    throw NewException.ArgumentOutOfRange(() => accessorType);
            }

            var builder = typeBuilder.DefineMethod(accessorPrefix + eventField.Name, EventAccessorBindingFlags, Types.Void, new[] { eventField.FieldType });
            builder.SetImplementationFlags(builder.GetMethodImplementationFlags() | MethodImplAttributes.Synchronized);

            var il = builder.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, eventField);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Call, delegateAction);

            il.Emit(OpCodes.Stfld, eventField);

            il.Emit(OpCodes.Ret);

            return builder;
        }

        public static void ImplementConstructor(this TypeBuilder typeBuilder, ConstructorInfo constructor)
        {
            if (typeBuilder == null)
                throw NewException.ArgumentNull(() => typeBuilder);
            if (constructor == null)
                throw NewException.ArgumentNull(() => constructor);

            var parameters = constructor.GetParameters();
            var paramTypes = parameters.Select(p => p.ParameterType).ToArray();

            var newAttrs = MethodAttributes.Public | (constructor.Attributes & ~MethodAttributes.MemberAccessMask);
            var builder = typeBuilder.DefineConstructor(newAttrs, constructor.CallingConvention, paramTypes);

            // exclude "this"
            for (int i = 0; i < parameters.Length; i++)
            {
                var param = parameters[i];
                builder.DefineParameter(i + 1, param.Attributes, param.Name);
            }

            var il = builder.GetILGenerator();
            // this, arg1, arg2, arg3
            for (int i = 0; i <= paramTypes.Length; i++)
                il.Emit(OpCodes.Ldarg, (short)i);

            il.Emit(OpCodes.Call, constructor);
            il.Emit(OpCodes.Ret);
        }

        public static void ImplementInterfaceMethod(this TypeBuilder typeBuilder, MethodInfo method)
        {
            if (typeBuilder == null)
                throw NewException.ArgumentNull(() => typeBuilder);
            if (method == null)
                throw NewException.ArgumentNull(() => method);
            if (!method.IsVirtual)
                throw NewException.Argument(() => method);

            var parameters = method.GetParameters();
            var paramTypes = parameters.Select(p => p.ParameterType).ToArray();

            var interfaceMethodName = method.DeclaringType.Name + "." + method.Name;
            var builder = typeBuilder.DefineMethod(interfaceMethodName, InterfaceMethodAttributes, method.ReturnType, paramTypes);

            var baseMethod = typeBuilder.BaseType.GetMethod(method.Name, paramTypes);

            // exclude "this"
            for (int i = 0; i < parameters.Length; i++)
            {
                var param = parameters[i];
                builder.DefineParameter(i + 1, param.Attributes, param.Name);
            }

            var il = builder.GetILGenerator();
            // this, arg1, arg2, arg3
            for (int i = 0; i <= paramTypes.Length; i++)
                il.Emit(OpCodes.Ldarg, (short)i);

            il.Emit(OpCodes.Call, baseMethod);
            il.Emit(OpCodes.Ret);

            typeBuilder.DefineMethodOverride(builder, method);
        }

    }
}
